/**
 * 
 */
package com.test.mathexpr;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.apache.log4j.Logger;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

/**
 * Generates questions on mathematics
 * Questions and related variables are read from template file in XML format. 
 * Variables are randomly selected from the list and questions along with answers are generated.
 * MathEval program is taken from the net with due credit to its creator. Lawrence PC Dol.
 * @author Ravi G
 * 
 */
public class MathQGen {
	
	public static final String encStr = "UTF-8";

	static final List<String> inputList = new ArrayList<String>();
	
	static final HashMap<String, List> varList = new HashMap<String,List>();
	
	static final List<TemplateQuestion> tempQuestions = new ArrayList<TemplateQuestion>();
	
	static final Map<String,TemplateQuestion> tempProcessed = new HashMap<String,TemplateQuestion>();
	
	static final String dummyVar = "{sDummyVar}";
	
	static final List<String> dummyVarList =  new ArrayList<String>();
	
	static final DecimalFormat decimalFormatter = (DecimalFormat) DecimalFormat.getInstance();
	
	static final DecimalFormat numberFormat = new DecimalFormat("#,##,###");
	
	static final Pattern pattern = Pattern.compile("\\{.*?\\}");
	
	public static int noOfRecords  = 10;
	
	public static Logger logger = Logger.getLogger(MathQGen.class);
	
	static 
	{
		dummyVarList.add(" ");
		dummyVarList.add("  ");
		decimalFormatter.applyPattern("#,###,###.00");
	}

	 
	/**
	 * Return random number between max and min
	 * @param min
	 * @param max
	 * @return
	 */
	public static int randInt(int min, int max) {
		Random rand = new Random();
		int randomNum = rand.nextInt((max - min) + 1) + min;
		return randomNum;
	}
	
	/**
	 * Check if the string has only number.
	 * @param str
	 * @return
	 */
	public static boolean isNumeric(String str)
	{
		try  
		  {  
		    double d = Double.parseDouble(str);  
		  }  
		  catch(NumberFormatException nfe)  
		  {  
		    return false;  
		  }  
		  return true;  
	}

	/**
	 * Popualte math eval with applicable list of variables.
	 * @param vars
	 * @return
	 */
	public static MathEval getMathEval(Map<String, List> vars) {
		
		MathEval meval = new MathEval();		
		Set<String> keys = vars.keySet();
		for (Iterator iterator = keys.iterator(); iterator.hasNext();) {
			String key = (String) iterator.next();
			List<String> values = vars.get(key);
			String value = values.get(randInt(0,values.size()-1));
			if ( isNumeric(value)){
				meval.setVariable(key, new Double(value));
			}
		}
		return meval;
	}
	
	/**
	 * Evaluate expression and format the numbers.
	 * 
	 * @param mathEval
	 * @param rString
	 * @return
	 */
	public static String evaluateExpr(MathEval mathEval,String rString){
		
		String expResult = "";
			
		double expr = mathEval.evaluate(rString);
		long iPart = (long)expr;
		double fPart = expr - iPart;
		
		if ( fPart == 0 ) {
			expResult = numberFormat.format(iPart);
			//logger.debug(rString + " >  " + expr + " as " + expResult );
		} else {
			expResult =   decimalFormatter.format(expr);
			//logger.debug(rString + " >  " + expr + " as " + expResult );
		}
		
		return expResult;
		
	}

	/**
	 * Load templates and related information from the file.
	 * 
	 * @param fileName
	 * @return
	 */
	public static List<TemplateQuestion> loadTemplates(String fileName){
		
		List<TemplateQuestion> templates = new ArrayList<TemplateQuestion>();
		
		File fXmlFile = new File(fileName);
		DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
		DocumentBuilder dBuilder;
		try {
		
			dBuilder = dbFactory.newDocumentBuilder();
			Document doc = dBuilder.parse(fXmlFile);
			
			doc.getDocumentElement().normalize();
			
			NodeList nList = doc.getElementsByTagName("template");

			for (int temp = 0; temp < nList.getLength(); temp++) {
		 
				Node nNode = nList.item(temp);
				TemplateQuestion que = new TemplateQuestion();
				
				if (nNode.getNodeType() == Node.ELEMENT_NODE) {
					Element eElement = (Element) nNode;
					que.setRawQuestion(eElement.getElementsByTagName("question").item(0).getTextContent());
					String rawAnswer = eElement.getElementsByTagName("answer").item(0).getTextContent(); 
					//rawAnswer = rawAnswer.replace("{", "");
					//rawAnswer = rawAnswer.replace("}", "");
					que.setRawAnswer(rawAnswer);
					
					// Explore if this has local set of variables defined.
					NodeList vList = eElement.getElementsByTagName("variable");
					
					if ( vList != null ) {
						for (int vCount = 0; vCount < vList.getLength(); vCount++) {
							Node vNode = vList.item(vCount);
							
							if (vNode.getNodeType() == Node.ELEMENT_NODE) {
								Element vElement = (Element) vNode;
								logger.debug("Question specific variables added  : " + vElement.getAttribute("name") + ":" +vElement.getTextContent() );								
								que.getVarList().put(vElement.getAttribute("name"), Arrays.asList(vElement.getTextContent().split(",")));
							}							
						}
					}
					templates.add(que);
				}
			}
			
			// Capture variables as well
			
			XPath xpath = XPathFactory.newInstance().newXPath();
			XPathExpression expr1 = xpath.compile("//templates/variable");
			NodeList vList = (NodeList)expr1.evaluate(doc, XPathConstants.NODESET);

			for (int thisCount = 0; thisCount < vList.getLength();thisCount++) {
				Node vNode = vList.item(thisCount);
				
				if (vNode.getNodeType() == Node.ELEMENT_NODE) {
					Element eElement = (Element) vNode;
					logger.debug(eElement.getAttribute("name") + ":" +eElement.getTextContent() );
					
					varList.put(eElement.getAttribute("name"), Arrays.asList(eElement.getTextContent().split(",")));
				}				
			}
			varList.put("sDummyVar", dummyVarList);

		} catch (ParserConfigurationException e) {
			e.printStackTrace();
		} catch (SAXException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} catch (XPathExpressionException e) {
			e.printStackTrace();
		}
				
		return templates;
	}
	
	public static String calculate(String inExpr,MathEval mathEval , HashMap<String, List> tempVarList){
		
		Matcher matcher = pattern.matcher(inExpr);
		Map<String,String> sMap = new HashMap<String,String>();
		StringBuffer sb = new StringBuffer();
		logger.debug("Processing " + inExpr);
		while (matcher.find()) {
			String result = matcher.group();

			String rString = result.replace("{", "");
			rString = rString.replace("}", "");
			rString = rString.trim();
			String expResult = "";
			
			List<String> values = tempVarList.get(rString);
			logger.debug("rString is " + rString + " with values " + values);			
			if ( sMap.containsKey(rString)){
				expResult = (String)sMap.get(rString);
			} else if ( values != null ) {
				expResult = values.get(randInt(0,values.size()-1));
				sMap.put(rString, expResult);
			}
			// Evaulate only if the result is numeric to take care of {x} and {y} conditions and we did not find any values as rString is expression.
			if ( isNumeric(expResult) || values == null ) {
				expResult = evaluateExpr(mathEval,rString);
			}
			matcher.appendReplacement(sb, expResult);
		}

		return sb.toString();
	}
	/**
	 * 
	 */
	public MathQGen() {
	}

	/**
	 * @param args
	 */
	public static void main(String[] args) {
		
		if ( args.length != 2 ) {
			System.err.println("Usage <File Name> <Number of recrods to generate> ....");
			System.exit(0);
		}
		
		noOfRecords = Integer.parseInt(args[1]);
		
		int questionId = 1 ;
		
		List<TemplateQuestion> templates = loadTemplates(args[0]);

		String inExpr = null;
		for (Iterator iterator = templates.iterator(); iterator.hasNext();) {
		
			TemplateQuestion tempQuestion = (TemplateQuestion) iterator.next() ;
			// Evaluate all text as one piece even if question and answers are in different variables. Append expression with dummy var.
			inExpr = tempQuestion.getRawQuestion() + "~" + tempQuestion.getRawAnswer() + dummyVar;
			
			for (int count = 0; count < noOfRecords; count++) {
							
				// Clone the varList and override locals in case question has its own varList, otherwise use originial.
				MathEval mathEval = null ;
				String result = null;
				HashMap<String, List> tempVarList = null;
				if ( tempQuestion.hasLocalVariables() == true ) {

					tempVarList = (HashMap<String,List>) varList.clone();
					
					for (Iterator iterator2 = tempQuestion.getVarList().keySet().iterator(); iterator2
							.hasNext();) {
						String varKey = (String) iterator2.next();
						List varValue = (List)  tempQuestion.getVarList().get(varKey);
						//logger.debug("Q related key " + varKey + " with values " + varValue );
						tempVarList.put(varKey, varValue);
					}
					mathEval = getMathEval(tempVarList);
				} else {
					tempVarList = varList;
					mathEval = getMathEval(tempVarList);
				}
				result = calculate(inExpr,mathEval,tempVarList);
				// Write content to file and close it.
				
				
			    logger.debug("** Result " + result );
				tempQuestion.setQuestion(result.substring(0,result.indexOf('~')));
				
				//String ansResult = calculate(tempQuestion.getRawAnswer()+dummyVar,mathEval,tempVarList);
				
				// evaluateExpr(mathEval,tempQuestion.getRawAnswer())
				tempQuestion.setAnswer(result.substring(result.indexOf('~') + 1 ));
				
				TemplateQuestion processed = new TemplateQuestion(tempQuestion);
				processed.setId(questionId++ + "");
				tempProcessed.put(processed.getQuestion(),processed);
			} // End of for loop for noOfRecords
		} // end of input expression
		
		
		// Now write result to file
		
		
		//logger.debug(result);				
		String newFileName = args[0].replace(".xml", "_result.xml");
		File outFile = new File(newFileName);
		
		
		
		try {
			FileOutputStream fos = new FileOutputStream(outFile);
			StringBuffer sbb = new StringBuffer();
			int tempCount = 1;
			for (Iterator iterator = tempProcessed.values().iterator(); iterator.hasNext();) {
				TemplateQuestion templateQuestion = (TemplateQuestion) iterator
						.next();
				sbb.append(tempCount++);
				sbb.append(".  " );
				sbb.append(templateQuestion.getQuestion());
				sbb.append("\n\n");
				sbb.append("उत्तर:  ");
				sbb.append(templateQuestion.getAnswer());
				sbb.append("\n==============================================================\n");
			}
			fos.write(sbb.toString().getBytes(encStr));
			fos.flush();
			fos.close();
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}

		logger.info("Output File :  " + newFileName );
	}
}
